package br.com.wagnerpaz.psychommand;

import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.LinkedList;
import java.util.List;

import br.com.projedata.util.EntityAttributeReader;
import br.com.projedata.util.StringGenericConverter;

/**
 * Defines a command to be used in
 * a command-based protocol.
 *  
 * @author Wagner Paz
 * @since  1.0
 */
public class Command
{
	private static final String SEPARATOR_CLOSE_PARAM = "}";
	private static final String SEPARATOR_OPEN_PARAM  = "{";
	private static final String SEPARATOR_NAME        = ">";
	private static final String SEPARATOR_PARAMS      = ",";
	private static final String SEPARATOR_OPEN_LIST   = "[";
	private static final String SEPARATOR_CLOSE_LIST  = "]";
	private static final String SEPARATOR_ITEM_LIST   = "|";
	private static final String KEYWORD_THIS          = "this";
	
	private String template;
	private String name;
	private String optimizedName;
	private String parameters;
	private List<String> paramNames;
	
	private boolean listType;
	private boolean optimized;
	
	/**
	 * Construct a new command.
	 * 
	 * @param template      A template that defines how the parameters
	 *		  		        should be write or read from the protocol data.
	 * @param trimTemplate  If true the template data ignores all white spaces.
	 * @param optimizedName If true the command name is written in optimized form.
	 */
	public Command(String template, boolean trimTemplate, String optimizedName)
	{
		if( template == null || template.equals("") )
		{
			throw new IllegalArgumentException("The template must be a consistent string.");
		}
		
		this.template      = template;
		this.optimizedName = optimizedName;
		
		if(trimTemplate)
		{
			this.template = this.template.replace(" ", "");
		}
		
		parseTemplate();
	}
	
	/**
	 * Retrieves the command name.
	 * 
	 * @param data The register data used to extract the name.
	 * @return The command name.
	 */
	public static String retriveName(String data)
	{
		return data.split(SEPARATOR_NAME)[0];
	}
	
	/**
	 * Reads the values expressed in the protocol data.
	 * The values will be assigned to the object members
	 * mapped on the template file.
	 * 
	 * @param <T>
	 * @param data     The protocol data.
	 * @param instance The object to assign the values.
	 * @return         A reference to the populated instance.
	 */
	@SuppressWarnings("unchecked")
	public <T> T read(String data, T instance)
	{
		String[] splitData = data.split(SEPARATOR_NAME);
		String   params    = splitData.length > 1 ? splitData[1] : "";
		
		if(paramNames != null && paramNames.size() > 0)
		{
			String[] values = params.split(SEPARATOR_PARAMS);
			
			if(paramNames.size() != values.length)
			{
				String eMessage = "The template of this command defines '{0}' parameters and was encontered '{1}'. Template used: '{3}'";
				eMessage        = MessageFormat.format(eMessage, paramNames.size(), values.length, template);
				
				throw new IllegalArgumentException(eMessage);
			}
			
			int count = 0;
			for(String paramName : paramNames)
			{
				try
				{
					Field field   = EntityAttributeReader.getFieldMultiLevel(instance.getClass(), paramName);
					Class<T> type = (Class<T>) field.getType();
					
					T valueConverted = StringGenericConverter.convert(type, values[count]);
					EntityAttributeReader.executeSetterMethod(paramName, instance, valueConverted);
				}
				catch (NoSuchFieldException e)
				{
					e.printStackTrace();
				}
				
				count++;
			}
		}
		
		return instance;
	}
	
	public String write(Object param)
	{
		StringBuffer commandStr = new StringBuffer();
		commandStr.append(optimized ? optimizedName : name);
		commandStr.append(SEPARATOR_NAME);
		
		if(listType)
		{
			commandStr.append(SEPARATOR_OPEN_LIST);
			
			List<?> list = (List<?>) param;
			
			boolean first = true;
			for(Object listItem : list)
			{
				if(!first)
				{
					commandStr.append(SEPARATOR_ITEM_LIST);
				}
				
				writeParams(commandStr, listItem);
				first = false;
			}
			
			commandStr.append(SEPARATOR_CLOSE_LIST);
		}
		else
		{
			writeParams(commandStr, param);
		}
		
		return commandStr.toString();
	}
	
	private void writeParams(StringBuffer commandStr, Object param)
	{
		boolean firstPass = true;
		for(String paramName : paramNames)
		{
			if(!firstPass)
			{
				commandStr.append(SEPARATOR_PARAMS);
			}
			
			if(paramName.equals(KEYWORD_THIS))
			{
				commandStr.append( param.toString() );
			}
			else
			{
				commandStr.append( EntityAttributeReader.executeGetterMethod(paramName, param).toString() );
			}
			
			firstPass = false;
		}
	}
	
	private void parseTemplate()
	{
		String[] splitSeparator = template.split(SEPARATOR_NAME);
		
		name = splitSeparator[0];
		
		if(splitSeparator.length > 1)
		{
			parameters = splitSeparator[1];
			listType   = parameters.charAt(0) == SEPARATOR_OPEN_LIST.charAt(0) && parameters.charAt( parameters.length()-1 ) == SEPARATOR_CLOSE_LIST.charAt(0);
			
			identifyParamNames(parameters);
		}
	}
	
	private void identifyParamNames(String params)
		throws IllegalArgumentException
	{
		paramNames = new LinkedList<String>();
		
		String notRead = params;
		while(true)
		{
			int openDec  = notRead.indexOf(SEPARATOR_OPEN_PARAM);
			int closeDec = notRead.indexOf(SEPARATOR_CLOSE_PARAM);
			
			if(openDec >= 0)
			{
				if(closeDec > 0)
				{
					String param = notRead.substring(openDec + 1, closeDec);
					paramNames.add(param);
					
					notRead = notRead.substring(closeDec + 1);
				}
				else
				{
					throw new IllegalArgumentException("The parameter list is not well formated.");
				}
			}
			else
			{
				break;
			}
		}
	}

	public String getTemplate()
	{
		return template;
	}

	public String getName()
	{
		return name;
	}

	public String getParameters()
	{
		return parameters;
	}

	public boolean isOptimized()
	{
		return optimized;
	}

	public void setOptimized(boolean optimized)
	{
		this.optimized = optimized;
	}

	public String getOptimizedName()
	{
		return optimizedName;
	}
}